package com.ease.util.reflection {

   import com.ease.util.MruCache;
   import com.ease.util.ProfilingManager;
   import com.ease.util.ReflectionUtil;
   import com.ease.util.Set;
   import com.ease.util.StringUtils;

   import flash.utils.Dictionary;
   import flash.utils.describeType;
   import flash.utils.getQualifiedClassName;

   /**
    * Provides methods to retrieve reflection information on classes, methods, properties
    * and meta tags.
    */
   public class ReflectionManager {
      // The singleton instance.
      private static var _instance:ReflectionManager;

      // The names of the tags that do not need to be included in the reflection info.
      // This is useful to reduce the size of the cached objects.
      // ArrayElementType is needed (see ReflectionUtil.getTaggedArrayElementType).
      private static const IGNORED_TAG_NAMES:Array = ["API", "Bindable", "Managed",
         "ChangeEvent", "NonCommittingChangeEvent", "Transient", "Inspectable",
         "__go_to_definition_help", "__go_to_ctor_definition_help"];

      private var _ignoredTagNames:Set = new Set();

      // The names of commonly used types. This is useful so that all references to these
      // strings use the same string objects.
      private static const COMMON_TYPE_NAMES:Array = ["Object", "Class", "String",
         "Boolean", "Number", "Array", "Date", "XML", "int", "uint", "void", "*",
         "mx.collections::ArrayCollection"];

      // key: Class or class name String, value: ReflectionInfo
      // Based on ProfilingManager.getObjectSize, the average size per 500 cached objects
      // is 1M. So this cache takes about 4M.
      // It is unlikely for this cache to get filled, the number I reached after visiting
      // most of the functionalities in vsphere-client MN was 700.
      private var _reflectionInfoByType:MruCache = new MruCache(2000);

      // key and value: common type names, and base classes and interface names.
      // This is useful so that the baseClassNames and interfaceNames properties of
      // ClassReflectionInfo share the same string objects.
      // Based on ProfilingManager.getObjectSize, this takes about 20k for 250 classes, which
      // was the number I reached after visiting most of the functionalities in
      // vsphere-client MN.
      private var _sharedTypeNames:Dictionary = new Dictionary();


      // key: Class name, value:Dictionary (propertyName --> PropertyReflectionInfo).

      // It contains the PropertyReflectionInfo objects of the Flex framework classes.
      // This is useful to reduce the size (tremendously) ClassReflectionInfo.propertyInfos,
      // which includes only the uninherited properties.
      // Based on ProfilingManager.getObjectSize, this takes about 100k for 70 classes, which
      // was the number I reached after visiting most of the functionalities in
      // vsphere-client MN.
      private var _flexPropInfosByClassName:Dictionary = new Dictionary();

      public function ReflectionManager(token:Class) {
         if (token != PrivateConstructorToken) {
            throw new Error("ReflectionManager cannot be instantiated directly.");
         }
         _ignoredTagNames.addCollection(IGNORED_TAG_NAMES);
         for each (var typeName:String in COMMON_TYPE_NAMES) {
            _sharedTypeNames[typeName] = typeName;
         }
      }

      /**
       * Retrieves the singleton instance of ReflectionManager.
       */
      public static function get instance():ReflectionManager {
         if (_instance == null) {
            _instance = new ReflectionManager(PrivateConstructorToken);
         }
         return _instance;
      }

      /**
       * Returns the class reflection info for the given class or object instance.
       */
      public function getClassInfo(object:Object):ClassReflectionInfo {
         var ri:ReflectionInfo = getReflectionInfo(object);
         return ri.classInfo;
      }

      /**
       * Returns the tag reflection infos for the given class or object instance, having
       * the given tag name.
       */
      public function getClassTagInfos(object:Object,
                                       metatag:String):Vector.<TagReflectionInfo> {
         var ci:ClassReflectionInfo = getClassInfo(object);
         var result:Vector.<TagReflectionInfo> = new Vector.<TagReflectionInfo>();
         for each (var tagInfo:TagReflectionInfo in ci.tagInfos) {
            if (tagInfo.name == metatag) {
               result.push(tagInfo);
            }
         }
         return result;
      }

      /**
       * Returns the property reflection infos for the given class or object instance,
       * having the given tag name.
       * Note that this returns only the unherited properties.
       */
      public function getTaggedPropertyInfos(object:Object,
                                             metatag:String):Vector.<PropertyReflectionInfo> {
         var ri:ReflectionInfo = getReflectionInfo(object);
         var result:Vector.<PropertyReflectionInfo> = new Vector.<PropertyReflectionInfo>();
         var uninheritedPropertyInfos:Vector.<PropertyReflectionInfo> =
            getUninheritedPropertyInfos(object, ri);
         for each (var pi:PropertyReflectionInfo in uninheritedPropertyInfos) {
            if (pi.hasTag(metatag)) {
               result.push(pi);
            }
         }
         return result;
      }

      /**
       * Returns the property in the given class or object instance, having the given name.
       */
      public function getPropertyInfo(object:Object,
                                      propName:String):PropertyReflectionInfo {
         var ri:ReflectionInfo = getReflectionInfo(object);
         var uninheritedPropertyInfos:Vector.<PropertyReflectionInfo> =
            getUninheritedPropertyInfos(object, ri);
         var result:PropertyReflectionInfo = findPropertyInfo(propName,
            uninheritedPropertyInfos);
         if (result != null) {
            return result;
         }
         // look up the property in the extended classes.
         for each (var className:String in ri.classInfo.baseClassNames) {
            var propDict:Dictionary = _flexPropInfosByClassName[className];
            result = (propDict != null) ? propDict[propName] : null;
            if (result != null) {
               return result;
            }
         }
         return null;
      }

      /**
       * Returns the properties in the given class or object instance, having the given
       * access mode.
       */
      public function getPropertyInfos(object:Object,
                                       accessMode:String):Vector.<PropertyReflectionInfo> {
         var result:Vector.<PropertyReflectionInfo> = new Vector.<PropertyReflectionInfo>();
         var ri:ReflectionInfo = getReflectionInfo(object);
         var uninheritedPropertyInfos:Vector.<PropertyReflectionInfo> =
            getUninheritedPropertyInfos(object, ri);
         for each (var pi:PropertyReflectionInfo in uninheritedPropertyInfos) {
            if (accessMode == ReflectionUtil.ACCESS_ANY || accessMode == pi.access) {
               result.push(pi);
            }
         }
         // append the flex base classes' properties.
         for each (var className:String in ri.classInfo.baseClassNames) {
            var propInfos:Dictionary = _flexPropInfosByClassName[className];
            if (propInfos == null) {
               continue;
            }
            for each (var obj:Object in propInfos) {
               pi = PropertyReflectionInfo(obj);
               if (accessMode == ReflectionUtil.ACCESS_ANY || accessMode == pi.access) {
                  result.push(pi);
               }
            }
         }

         return result;
      }

      /**
       * Returns the methods in the given class or object instance, having the given
       * tag name.
       */
      public function getTaggedMethodInfos(object:Object,
                                           metatag:String):Vector.<MethodReflectionInfo> {
         var ri:ReflectionInfo = getReflectionInfo(object);
         var result:Vector.<MethodReflectionInfo> = new Vector.<MethodReflectionInfo>();
         for each (var mi:MethodReflectionInfo in ri.taggedMethodInfos) {
            if (mi.hasTag(metatag)) {
               result.push(mi);
            }
         }
         return result;
      }

      /**
       * Returns the method in the given class or object instance, having the given name.
       * Only tagged methods are considered.
       */
      public function getTaggedMethodInfo(object:Object,
                                          methodName:String):MethodReflectionInfo {
         var ri:ReflectionInfo = getReflectionInfo(object);
         for each (var mi:MethodReflectionInfo in ri.taggedMethodInfos) {
            if (mi.name == methodName) {
               return mi;
            }
         }
         return null;
      }

      /**
       * Returns the uninherited property infos for the given object.
       * The result includes the objects in the given ReflectionInfo's
       * uninheritedPropertyInfos property, as well as the objects in
       * _flexPropInfosByClassName if the given object is of a Flex type.
       *
       * The latter step is needed because the properties of the Flex types are
       * stored separately in _flexPropInfosByClassName so that they are shared.
       *
       * The former step is needed even for Flex types because the <code>declaringType</code>
       * attribute in properties xml returned from describeType may be
       * missing (seemingly for properties declared as "var", and "internal" properties).
       * In this case these properties are stored in ReflectionInfo.uninheritedPropertyInfos
       * in appendPropertyInfo.
       */
      private function getUninheritedPropertyInfos(object:Object,
                                                   ri:ReflectionInfo):Vector.<PropertyReflectionInfo> {
         var className:String = getQualifiedClassName(object);
         var propInfos:Dictionary = _flexPropInfosByClassName[className];
         if (propInfos == null) {
            return ri.uninheritedPropertyInfos;
         }
         var result:Vector.<PropertyReflectionInfo>;
         if (ri.uninheritedPropertyInfos != null) {
            result = ri.uninheritedPropertyInfos.concat();
         } else {
            result = new Vector.<PropertyReflectionInfo>();
         }
         for each (var rawInfo:Object in propInfos) {
            var pi:PropertyReflectionInfo = PropertyReflectionInfo(rawInfo);
            result.push(pi);
         }
         return result;
      }

      private function getTypeDescription(object:Object):XML {
         var typeXml:XML = describeType(object);
         return typeXml;
      }

      private function getReflectionInfo(object:Object):ReflectionInfo {
         var cacheKey:Object = getCacheKey(object);
         var ri:ReflectionInfo = _reflectionInfoByType.getValue(cacheKey);
         if (ri == null) {
            ri = createReflectionInfo(object);
            _reflectionInfoByType.add(cacheKey, ri);
            /* if ((_reflectionInfoByType.count % 20) == 0) {
            traceCacheSize();
            } */
         }
         return ri;
      }

      /**
       * Returns the reflection info cache key for the given object.
       */
      private function getCacheKey(object:Object):Object {
         if (object is Class) {
            return object as Class;
         }
         var cacheKey:Object;
         try {
            cacheKey = object.constructor;
         } catch (error:Error) {
            // Some classes that extend flash.utils.Proxy do not handle properly the
            // properties defined in Object.
            // For example see ListCollectionView.getProperty, which throws an exception
            // when called with "constructor" as its argument because it supports
            // only int arguments.
         }
         if (cacheKey == null) {
            cacheKey = getQualifiedClassName(object);
         }
         return cacheKey;
      }

      private function createReflectionInfo(object:Object):ReflectionInfo {
         var typeXml:XML = getTypeDescription(object);
         var ri:ReflectionInfo = new ReflectionInfo();
         ri.classInfo = createClassInfo(typeXml);
         ri.uninheritedPropertyInfos = createPropertyInfos(typeXml);
         ri.taggedMethodInfos = createTaggedMethodInfos(typeXml);
         return ri;
      }

      private function createClassInfo(typeXml:XML):ClassReflectionInfo {
         var classXml:XML;
         if (typeXml.@isStatic == "true") {
            // The passed-in object is a Class, so need to look inside the type
            // description's 'factory' section.
            var xmlList:XMLList = typeXml.factory;
            classXml = xmlList[0];
         } else {
            classXml = typeXml;
         }
         var ci:ClassReflectionInfo = new ClassReflectionInfo();
         ci.tagInfos = getTagInfos(classXml.metadata);

         var baseClassesXml:XMLList = classXml.extendsClass.@type;
         ci.baseClassNames = getTypeNames(baseClassesXml);

         var interfacesXml:XMLList = classXml.implementsInterface.@type;
         ci.interfaceNames = getTypeNames(interfacesXml);

         return ci;
      }

      private function getTypeNames(typesXml:XMLList):Vector.<String> {
         if (typesXml.length() == 0) {
            return null;
         }
         var typeNames:Vector.<String> = new Vector.<String>();
         for each (var nameXml:XML in typesXml) {
            var name:String = String(nameXml);
            name = sharedClassName(name, true);
            typeNames.push(name);
         }
         if (typeNames.length == 0) {
            return null;
         }
         return typeNames;
      }

      private function sharedClassName(name:String, cache:Boolean = false):String {
         if (StringUtils.isNullorEmpty(name)) {
            return null;
         }
         var result:String = _sharedTypeNames[name];
         if (result == null) {
            result = name;
            if (cache) {
               _sharedTypeNames[name] = name;
            }
         }
         return result;
      }

      private function createPropertyInfos(typeXml:XML):Vector.<PropertyReflectionInfo> {
         var propsXml:XMLList = typeXml..accessor + typeXml..variable;
         var propInfos:Vector.<PropertyReflectionInfo> =
            new Vector.<PropertyReflectionInfo>();
         for each (var propXml:XML in propsXml) {
            appendPropertyInfo(propXml, propInfos);
         }
         if (propInfos.length == 0) {
            propInfos = null;
         }
         return propInfos;
      }

      private function createTaggedMethodInfos(typeXml:XML):Vector.<MethodReflectionInfo> {
         var methodsXml:XMLList = typeXml..method.(hasOwnProperty("metadata"));
         var methodInfos:Vector.<MethodReflectionInfo> = new Vector.<MethodReflectionInfo>();
         for each (var methodXml:XML in methodsXml) {
            var mi:MethodReflectionInfo = newTaggedMethodInfo(methodXml);
            if (mi == null) {
               continue;
            }
            methodInfos.push(mi);
         }
         if (methodInfos.length == 0) {
            methodInfos = null;
         }
         return methodInfos;
      }

      private function appendPropertyInfo(propXml:XML,
                                          propInfos:Vector.<PropertyReflectionInfo>):void {
         var declaringType:String = propXml.@declaredBy;
         var propName:String = propXml.@name;
         var flexPropInfos:Dictionary = _flexPropInfosByClassName[declaringType];
         // We check if the property exists even if the class is in the cache because
         // overriden properties have the <code>declaredBy</code> reflection attribute
         // set to the overriding class and not the root class that declares the property.
         // For example, when we do the reflection of the main app class, and given that
         // mx.core.Application overrides the UIComponent "id" property, then declaringType
         // will be returned as mx.core.Application and not UIComponent, and the id property
         // will be cached as a property in mx.core.Application.
         // The check is necessary so that when we reflect on a subclass of UIComponent, the
         // id property is added to the UIComponent cached properties.
         if (flexPropInfos != null && flexPropInfos[propName] != null) {
            return;
         }
         var pi:PropertyReflectionInfo = newPropertyInfo(propXml);
         var isFlexType:Boolean = ReflectionUtil.isFlexTypeName(declaringType)
            || declaringType == "Class";
         if (!isFlexType) {
            propInfos.push(pi);
            return;
         }
         if (flexPropInfos == null) {
            flexPropInfos = new Dictionary();
            declaringType = sharedClassName(declaringType);
            _flexPropInfosByClassName[declaringType] = flexPropInfos;
         }
         flexPropInfos[pi.name] = pi;
      }

      private function newPropertyInfo(propXml:XML):PropertyReflectionInfo {
         var pi:PropertyReflectionInfo = new PropertyReflectionInfo();
         setMemberProperties(pi, propXml);
         pi.typeName = sharedClassName(propXml.@type);
         if (propXml.name() == "variable") {
            pi.access = ReflectionUtil.ACCESS_READWRITE;
            pi.isVariable = true;
         } else {
            pi.access = propXml.@access;
            pi.isVariable = false;
         }
         pi.tagInfos = getTagInfos(propXml.metadata);

         return pi;
      }

      private function newTaggedMethodInfo(methodXml:XML):MethodReflectionInfo {
         var tagInfos:Vector.<TagReflectionInfo> = getTagInfos(methodXml.metadata);
         if (tagInfos == null || tagInfos.length == 0) {
            return null;
         }
         var mi:MethodReflectionInfo = new MethodReflectionInfo();
         setMemberProperties(mi, methodXml);

         //compute method arg type names.
         var params:XMLList = methodXml.parameter;
         if (params.length() != 0) {
            var argTypeNames:Vector.<String> = new Vector.<String>();
            for each (var param:XML in params) {
               var typeName:String = sharedClassName(param.@type);
               argTypeNames.push(typeName);
            }
            mi.argTypeNames = argTypeNames;
         }

         mi.tagInfos = tagInfos;

         return mi;
      }

      private function setMemberProperties(info:MemberReflectionInfo, memberXml:XML):void {
         info.name = memberXml.@name;
         info.declaringTypeName = sharedClassName(memberXml.@declaredBy);
      }

      private function getTagInfos(metadataXmlList:XMLList):Vector.<TagReflectionInfo> {
         if (metadataXmlList.length() == 0) {
            return null;
         }
         var result:Vector.<TagReflectionInfo> = new Vector.<TagReflectionInfo>();
         for each (var metadataXml:XML in metadataXmlList) {
            var tagName:String = metadataXml.@name;
            if (_ignoredTagNames.contains(tagName)) {
               continue;
            }
            var tagInfo:TagReflectionInfo = new TagReflectionInfo();
            tagInfo.name = tagName;
            for each (var argXml:XML in metadataXml.arg) {
               var argInfo:TagArgReflectionInfo = new TagArgReflectionInfo();
               argInfo.key = StringUtils.nullIfEmpty(argXml.@key);
               argInfo.value = argXml.@value;
               tagInfo.addArg(argInfo);
            }

            result.push(tagInfo);
         }
         if (result.length == 0) {
            result = null;
         }
         return result;
      }

      private function findPropertyInfo(propName:String,
                                        infos:Vector.<PropertyReflectionInfo>):PropertyReflectionInfo {
         for each (var pi:PropertyReflectionInfo in infos) {
            if (pi.name == propName) {
               return pi;
            }
         }
         return null;
      }

      /*
      // Useful in debugging the cache sizes.
      private function traceCacheSize():void {
      var size:int = ProfilingManager.instance.getObjectSize(_reflectionInfoByType);
      trace("--- _reflectionInfoByType: size=" + size
      + ", count=" + _reflectionInfoByType.count);

      size = getSize(_sharedTypeNames)
      + ProfilingManager.instance.getObjectSize(MixedUtil.getKeys(_sharedTypeNames));
      trace("--- _sharedTypeNames: size=" + size
      + ", count=" + MixedUtil.getDictionaryLength(_sharedTypeNames));

      size = ProfilingManager.instance.getObjectSize(_flexPropInfosByClassName);
      trace("--- _flexPropInfosByClassName: size=" + size
      + ", count=" + MixedUtil.getDictionaryLength(_flexPropInfosByClassName));
      }
      */
   }
}


import com.ease.util.ProfilingManager;
import com.ease.util.reflection.ClassReflectionInfo;
import com.ease.util.reflection.MethodReflectionInfo;
import com.ease.util.reflection.PropertyReflectionInfo;

class PrivateConstructorToken {}


class ReflectionInfo {
   public var classInfo:ClassReflectionInfo;

   public var uninheritedPropertyInfos:Vector.<PropertyReflectionInfo>;

   public var taggedMethodInfos:Vector.<MethodReflectionInfo>;

   /**
    * @see ProfilingManager.getObjectSize.
    */
   public function getNestedSize():int {
      var size1:int = ProfilingManager.instance.getObjectSize(this.classInfo);
      var size2:int = ProfilingManager.instance.getObjectSize(this.uninheritedPropertyInfos);
      var size3:int = ProfilingManager.instance.getObjectSize(this.taggedMethodInfos);
      return  size1 + size2 + size3;
   }
}